#include "httpd.h"
#include "http_config.h"
#include "http_core.h"
#include "http_log.h"
#include "http_main.h"
#include "http_protocol.h"
#include "http_request.h"
#include "util_script.h"
#include "http_connection.h"

#include "apr_strings.h"

#include <stdio.h>
#include <wand/magick-wand.h>

#define radius2index(r, cglcf) (r-(cglcf)->min_radius)/(cglcf)->step_radius

module AP_MODULE_DECLARE_DATA graphics_module;

// request format
// /circles/<fg>-<bg>-<diameter>.gif
// /lines/<orientation>-color1-color2-length.gif
//
// where fg, bg, color1, color2 - color in 6-hexdigits format
// diameter - decimal circle diameter
// orientation - "v" (vertical) or "h" (horisontal)
// length - line length

enum colors { RED, GREEN, BLUE };

enum graphics { CIRCLE = 1, GRADIENT_LINE, THUMB };

#ifndef LOGLEVEL
#define LOGLEVEL APLOG_INFO
#endif

#define REPORT_ERROR(s) ap_log_error(APLOG_MARK, LOGLEVEL, 0, r->server, "mod_graphics: %s", s);

typedef struct {
    //circle params
    uint32_t   max_radius;
    uint32_t   min_radius;
    uint32_t   step_radius;
    unsigned char** circle_templates;
    size_t* circle_sizes;
    int           enable;

    // line params
    uint32_t   max_len;

    // thumb params
    char * path_images;
} http_graphics_conf_t;

static int http_circle_gif_init(http_graphics_conf_t *cf);

static void* http_circle_gif_template(int req_radius, size_t* image_length_ptr, 
	MagickWand *wand, PixelWand *bg_wand, PixelWand *fg_wand, DrawingWand *dwand);

static void http_circle_gif_colorize(unsigned char *image, unsigned char *bg, unsigned char *fg);

static http_graphics_conf_t *our_dconfig(const request_rec *r)
{
    return (http_graphics_conf_t  *) ap_get_module_config(r->per_dir_config, &graphics_module);
}

static http_graphics_conf_t *our_cconfig(const conn_rec *c)
{
    return (http_graphics_conf_t *) ap_get_module_config(c->conn_config, &graphics_module);
}

// thumb handler
static int http_thumb_handler(request_rec *r)
{
    // Format: /thumb/{width}x{height}{options}/{local-path-to-image}
    //

    http_graphics_conf_t  *conf = our_dconfig(r);

    int len = strlen(r->path_info);
    char *digit;

    //check for minumum len
    if(len<6) // strlen(/1x1/p)=6
    {
        REPORT_ERROR("wrong query length");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    digit = (char *)r->path_info+1; // skip lead "/"

    int power;
    size_t i=0;

    const int max_dimension = 5;  // 99999 max
    char dim[max_dimension+1] ; 

    while (*digit >= '0' && *digit <= '9')
    {
        dim[i] = *digit++;
        if(++i >= max_dimension) 
            break;
    }
    dim[i]=0;
    uint32_t width = atoi(dim);

    if(*digit!='x')
    {
        REPORT_ERROR("wrong width size");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    digit++ ; // skip 'x'

    i = 0;
    while (*digit >= '0' && *digit <= '9')
    {
        dim[i] = *digit++;
        if(++i >= max_dimension) 
            break;
    }
    dim[i]=0;
    uint32_t height = atoi(dim);

    if(*digit!='/' && *digit!='c' && *digit!='C')
    {
        REPORT_ERROR("wrong height size");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    int is_crop = 0;
    if(*digit=='c' || *digit=='C')
    {
        is_crop = 1;
        digit++;
    }

    int path_len = strlen(digit);

    char *path_prefix = conf->path_images; // config param

    MagickWand *wand = NULL;
    unsigned char *image_blob = NULL;

    char * filename = (char *) malloc(strlen(path_prefix) + path_len + 1/* '/' */ + 1/* '\0' */);
    if (!filename)
    {
        REPORT_ERROR("memory problem");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    sprintf(filename,"%s/%s", path_prefix, digit);

#define CLEAN_THUMB \
    if(filename) \
        free(filename); \
    if(image_blob) \
        MagickRelinquishMemory(image_blob); \
    if(wand) \
        DestroyMagickWand( wand );

    wand = NewMagickWand();
    if( !MagickReadImage(wand, filename) )
    {
        REPORT_ERROR("open image error");
        CLEAN_THUMB;
        return DECLINED;
    }

    // real image width and heihgt
    unsigned long a = MagickGetImageWidth(wand) ;
    unsigned long b = MagickGetImageHeight(wand) ;

    double ratio_image = 1.0 * a / b ;

    if( width<a || height<b )  // don't resize if wifth and heihgt are greater than real sizes
    {
        if ( ! is_crop )   // thumb
        {
            if ( width / ratio_image <= height )
                height = width / ratio_image;
            else
                width = height * ratio_image;

            MagickResizeImage(wand, width, height, LanczosFilter, 1.0);
        }
        else // crop
        {
            long x_crop=0, y_crop=0;
            if ( width / ratio_image > height )
                y_crop = width / ratio_image - height;
            else
                x_crop = height * ratio_image - width;

            MagickResizeImage(wand, width +  x_crop, height + y_crop, LanczosFilter, 1.0);
            MagickCropImage(wand, width, height, x_crop/2, y_crop/2);
        }
    }

    // get image blob
    size_t image_len;
    image_blob = MagickGetImageBlob(wand, &image_len);
    if( image_blob == NULL )
    {
        REPORT_ERROR("error generating image blob");
        CLEAN_THUMB;
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // set headers
    ap_set_content_type(r, "image/gif");
    ap_set_content_length(r, image_len);

    if (r->header_only) 
    {
        CLEAN_THUMB;
        return OK;
    }

    // allocate memory from apache pool and copy blob image there
    unsigned char *image = (unsigned char *) apr_pcalloc(r->pool, image_len);
    if( image == NULL )
    {
        REPORT_ERROR("memory allocating error");
        CLEAN_THUMB;
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    memcpy(image, image_blob, image_len);

    // Send image content
    ap_rwrite(image,image_len,r);

    return OK;
}

// handler for circles
static int http_circles_handler(request_rec *r)
{
    u_int bg_color, fg_color, req_radius = 0;
    size_t i;
    int power;
    char *digit;
    unsigned char bg[3]; 
    unsigned char fg[3];

    http_graphics_conf_t  *cglcf = our_dconfig(r);
    int len = strlen(r->path_info);

    //check for minumum len
    if(len<20) // strlen(/000000-000000-1.gif)=20
    {
        REPORT_ERROR("wrong query length");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    digit = (char *)r->path_info + len - 1;

    if (!(*digit-- == 'f' && *digit-- == 'i' && *digit-- == 'g' && *digit-- == '.')) 
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    //calculate radius
    for(i=0, power=1; *digit >= '0' && *digit <= '9' && i < (len - 7 - 7 - 4); i++, power *= 10) 
    {
        req_radius += (*digit-- - '0')*power;
    }

    // format check
    if(*digit!='-')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // get foregraund color
    fg_color   = strtol(digit -= 6, NULL, 16);
    digit--;

    // format check
    if(*digit!='-')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    
    // get background color
    bg_color   = strtol(digit -= 6, NULL, 16);
    digit--;

    // format check
    if(*digit!='/')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    bg[RED] = bg_color >> 16;
    bg[GREEN] = bg_color >> 8;
    bg[BLUE] = bg_color;

    fg[RED] = fg_color >> 16;
    fg[GREEN] = fg_color >> 8;
    fg[BLUE] = fg_color;

    //check data
    if (req_radius < cglcf->min_radius || req_radius > cglcf->max_radius) 
    {
        REPORT_ERROR("wrong radius");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    int radius_index = radius2index(req_radius, cglcf);

    int content_length_n = cglcf->circle_sizes[radius_index];

    //set header
    ap_set_content_type(r, "image/gif");
    ap_set_content_length(r, content_length_n);
    if (r->header_only) 
    {
        return OK;
    }

    // Allocating memory from apache
    unsigned char *image;
    image = (unsigned char *) apr_pcalloc(r->pool, content_length_n);
    if( image == NULL )
    {
        REPORT_ERROR("memory allocating error");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // copy circles from template
    memcpy(image, cglcf->circle_templates[radius_index], content_length_n);

    // paint circle
    http_circle_gif_colorize(image, bg, fg);

    // write image to client
    ap_rwrite(image,cglcf->circle_sizes[radius_index],r);

    return OK;
}

// handler for lines
static int http_lines_handler(request_rec *r)
{
    // request format:
    // /lines/<orientation>-color1-color2-length.gif
    // orientation is v or h
    // color1, color2 in hex RGB format
    // length is decimal line length

    int len = strlen(r->path_info);
    char *digit;

    //check for minumum len
    if(len<22) // strlen(/v-000000-000000-1.gif)=22
    {
        REPORT_ERROR("wrong query length");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    digit = (char *)r->path_info + len - 1;

    if (!(*digit-- == 'f' && *digit-- == 'i' && *digit-- == 'g' && *digit-- == '.')) 
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    uint32_t line_length = 0;
    size_t i;
    int power;

    // calculate line length
    for(i=0, power=1; *digit >= '0' && *digit <= '9' && i < (len - 7 - 7 - 2 - 4); i++, power *= 10) 
    {
        line_length += (*digit-- - '0')*power;
    }

    // format check
    if(*digit!='-')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // get color2
    uint32_t color2   = strtol(digit -= 6, NULL, 16);
    digit--;

    // format check
    if(*digit!='-')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    
    // get color1
    uint32_t color1   = strtol(digit -= 6, NULL, 16);
    digit--;

    // format check
    if(*digit!='-')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    digit--;

    // get line direction
    char direction = *digit;

    // format check
    if(*(digit-1)!='/')
    {
        REPORT_ERROR("wrong query format");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // check input data
    http_graphics_conf_t  *cfg = our_dconfig(r);
    if( line_length<1 || line_length > cfg->max_len)
    {
        REPORT_ERROR("wrong line length");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    if((color1>0xffffff) || (color2>0xffffff))
    {
        REPORT_ERROR("unrecognized color");
        return HTTP_INTERNAL_SERVER_ERROR;
    }
    if( direction!='h' && direction!='H' &&direction!='v' &&direction!='H' )
    {
        REPORT_ERROR("unrecognized direction");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // make line with ImageMagick
    MagickWand *wand = NULL;
    PixelWand *bg_wand = NULL;
    wand = NewMagickWand();
    MagickSetSize(wand, 1, line_length);
    char gradient_line_descr[25];
    snprintf(gradient_line_descr, 25, "gradient:#%.6x-#%.6x", color1, color2);
    MagickReadImage(wand, gradient_line_descr);
    MagickSetImageFormat(wand, "gif");

    // rotate if direction is horisontal
    if (direction=='h' || direction=='H')
    {
        bg_wand = NewPixelWand();
        MagickRotateImage(wand, bg_wand, -90.0);
    }

    // get image blob
    size_t image_len;
    unsigned char *image_blob = MagickGetImageBlob(wand, &image_len);
    if( image_blob == NULL )
    {
        REPORT_ERROR("error generating line image blob");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    // set headers
    ap_set_content_type(r, "image/gif");
    ap_set_content_length(r, image_len);

    if (r->header_only) 
    {
        return OK;
    }

    // allocate memory from apache pool and copy blob image there
    unsigned char *image = (unsigned char *) apr_pcalloc(r->pool, image_len);
    if( image == NULL )
    {
        REPORT_ERROR("memory allocating error");
        return HTTP_INTERNAL_SERVER_ERROR;
    }

    memcpy(image, image_blob, image_len);

    // Send image content
    ap_rwrite(image,image_len,r);

    // clean
    if(image_blob)
        MagickRelinquishMemory(image_blob);
    if(wand)
        DestroyMagickWand( wand );
    if(bg_wand)
        DestroyPixelWand( bg_wand );

    return OK;
}

// handler for all graphics
static int http_graphics_handler(request_rec *r)
{
    int figure = 0;
    if ( strcmp(r->handler, "circles")==0 ) 
    {
        figure = CIRCLE;
    }
    else if ( strcmp(r->handler, "lines")==0 )
    {
        figure = GRADIENT_LINE;
    }
    else if ( strcmp(r->handler, "thumb")==0 )
    {
        figure = THUMB;
    }

    if( !figure )
    {
        return DECLINED;  // No circles nor lines
    }

    if(r->method_number != M_GET)
    {
        return HTTP_METHOD_NOT_ALLOWED;
    }

    http_graphics_conf_t  *cglcf = our_dconfig(r);

    //init on first time
    if(!cglcf->enable)
        http_circle_gif_init(cglcf);

    if(figure==CIRCLE)
        return http_circles_handler(r);
    else if (figure==GRADIENT_LINE)
        return http_lines_handler(r);
    else if (figure==THUMB)
        return http_thumb_handler(r);
    else
        return HTTP_INTERNAL_SERVER_ERROR;

}

static void *http_graphics_create_dir_config(apr_pool_t *p, char *dirspec)
{
    http_graphics_conf_t *cfg;
    cfg = (http_graphics_conf_t *) apr_pcalloc(p, sizeof(http_graphics_conf_t));

    // circles parameters
    cfg->min_radius = 10;
    cfg->max_radius = 20;
    cfg->step_radius = 2;
    cfg->circle_sizes = NULL;
    cfg->circle_templates = NULL;
    cfg->enable = 0;

    // lines parameters
    cfg->max_len = 1000;

    // thumb params
    cfg->path_images = NULL;

    return (void *) cfg;
}

static void *http_graphics_merge_dir_config(apr_pool_t *p, void *parent_conf,
                                      void *newloc_conf)
{
    http_graphics_conf_t *prev = (http_graphics_conf_t *) parent_conf;
    http_graphics_conf_t *conf = (http_graphics_conf_t *) newloc_conf;

    http_graphics_conf_t *merged_config = (http_graphics_conf_t *) apr_pcalloc(p, sizeof(http_graphics_conf_t));

    merged_config->min_radius = conf->min_radius?conf->min_radius:prev->min_radius;
    merged_config->max_radius = conf->max_radius?conf->max_radius:prev->max_radius;
    merged_config->step_radius = conf->step_radius?conf->step_radius:prev->step_radius;

    merged_config->max_len = conf->max_len?conf->max_len:prev->max_len;

    merged_config->path_images = conf->path_images?conf->path_images:prev->path_images;

    if (conf->min_radius < 1) {
        return NULL;
    }
    if (conf->max_radius < conf->min_radius) {
        return NULL;
    }

    return (void *) merged_config;
}

static void *http_graphics_create_server_config(apr_pool_t *p, server_rec *s)
{
    http_graphics_conf_t *cfg;
    cfg = (http_graphics_conf_t *) apr_pcalloc(p, sizeof(http_graphics_conf_t));
    
    // circles parameters
    cfg->min_radius = 10;
    cfg->max_radius = 20;
    cfg->step_radius = 2;
    cfg->circle_sizes = NULL;
    cfg->circle_templates = NULL;
    cfg->enable = 0;

    // lines parameters
    cfg->max_len = 1000;

    // thumb parameters
    cfg->path_images = NULL;

    return (void *)cfg;
}

static void *http_graphics_merge_server_config(apr_pool_t *p, void *server1_conf, void *server2_conf)
{
    http_graphics_conf_t *prev = (http_graphics_conf_t *) server1_conf;
    http_graphics_conf_t *conf = (http_graphics_conf_t *) server2_conf;

    http_graphics_conf_t *merged_config = (http_graphics_conf_t *) apr_pcalloc(p, sizeof(http_graphics_conf_t));

    merged_config->min_radius = conf->min_radius?conf->min_radius:prev->min_radius;
    merged_config->max_radius = conf->max_radius?conf->max_radius:prev->max_radius;
    merged_config->step_radius = conf->step_radius?conf->step_radius:prev->step_radius;

    merged_config->max_len = conf->max_len?conf->max_len:prev->max_len;

    merged_config->path_images = conf->path_images?conf->path_images:prev->path_images;

    if (conf->min_radius < 1) {
        return NULL;
    }
    if (conf->max_radius < conf->min_radius) {
        return NULL;
    }

    return (void *) merged_config;
}

static void http_graphics_register_hooks(apr_pool_t *p)
{
    ap_hook_handler(http_graphics_handler, NULL, NULL, APR_HOOK_MIDDLE);
}

static const char *add_min_radius(cmd_parms *cmd, void *mconf, const char *f)
{
    http_graphics_conf_t *conf = mconf;
    conf->min_radius = atoi(f);
    return NULL;
}
static const char *add_max_radius(cmd_parms *cmd, void *mconf, const char *f)
{
    http_graphics_conf_t *conf = mconf;
    conf->max_radius = atoi(f);
    return NULL;
}
static const char *add_step_radius(cmd_parms *cmd, void *mconf, const char *f)
{
    http_graphics_conf_t *conf = mconf;
    conf->step_radius = atoi(f);
    return NULL;
}

static const char *add_max_len(cmd_parms *cmd, void *mconf, const char *f)
{
    http_graphics_conf_t *conf = mconf;
    conf->max_len = atoi(f);
    return NULL;
}

static const char *add_path_images(cmd_parms *cmd, void *mconf, const char *f)
{
    http_graphics_conf_t *conf = mconf;
    conf->path_images = (char *) malloc(strlen(f)+1);
    if(conf->path_images)
        strcpy(conf->path_images, f);
    return NULL;
}

static const command_rec circle_cmds[] =
{
        AP_INIT_TAKE1("CircleMinRadius", add_min_radius, NULL, OR_LIMIT, "circle minumum radius"),
        AP_INIT_TAKE1("CircleMaxRadius", add_max_radius, NULL, OR_LIMIT, "circle maximum radius"),
        AP_INIT_TAKE1("CircleStepRadius", add_step_radius, NULL, OR_LIMIT, "circle step radius"),

        AP_INIT_TAKE1("LineMaxLen", add_max_len, NULL, OR_LIMIT, "maximum gradient line length"),

        AP_INIT_TAKE1("ImagesPath", add_path_images, NULL, OR_LIMIT, "path to images for thumbnails"),

        {NULL}
};

module AP_MODULE_DECLARE_DATA graphics_module =
{
    STANDARD20_MODULE_STUFF,
    http_graphics_create_dir_config,    /* per-directory config creator */
    http_graphics_merge_dir_config,     /* per-directory config merge   */
    http_graphics_create_server_config, /* server config creator */
    http_graphics_merge_server_config,  /* server config merge */
    circle_cmds,                      /* command table */
    http_graphics_register_hooks,       /* set up other request processing hooks */
};

static int
http_circle_gif_init(http_graphics_conf_t *cglcf)
{
  u_int i;
  MagickWand *wand;
  PixelWand *bg_wand, *fg_wand;
  DrawingWand *dwand;

  wand = NewMagickWand();
  bg_wand = NewPixelWand();
  fg_wand = NewPixelWand();
  dwand = NewDrawingWand();
  if ((cglcf->circle_templates = malloc((1+radius2index(cglcf->max_radius, cglcf))*sizeof(unsigned char*))) == NULL ||
	  (cglcf->circle_sizes = malloc((1+radius2index(cglcf->max_radius, cglcf))*sizeof(size_t))) == NULL) {
    perror("malloc()");
    return -1;
  }
  for (i=0;i<=radius2index(cglcf->max_radius, cglcf);i++) {
    cglcf->circle_templates[i] = http_circle_gif_template(cglcf->min_radius+i*cglcf->step_radius, &cglcf->circle_sizes[i], 
            wand, bg_wand, fg_wand, dwand);
  }
  DestroyMagickWand( wand );
  DestroyPixelWand( fg_wand );
  DestroyPixelWand( bg_wand );
  DestroyDrawingWand( dwand );
  cglcf->enable=1;
  return i;
}

// build a B&W circle GIF in memory and return a pointer to it.
static void* http_circle_gif_template(int req_radius, size_t* image_length_ptr, 
	MagickWand *wand, PixelWand *bg_wand, PixelWand *fg_wand, DrawingWand *dwand)
{
  float radius = req_radius - 0.5;

  ClearDrawingWand(dwand);
  PixelSetColor(bg_wand, "#000000");
  PixelSetColor(fg_wand, "#ffffff");
  MagickNewImage(wand, req_radius*2, req_radius*2, bg_wand);
  DrawSetFillColor(dwand, fg_wand);
  DrawCircle(dwand, radius, radius, 0, radius);
  MagickDrawImage(wand, dwand);
  MagickSetImageFormat(wand, "gif");

  unsigned char *image = MagickGetImageBlob(wand, image_length_ptr);

  MagickRemoveImage(wand);
  return image;
}

static void http_circle_gif_colorize(unsigned char *image, unsigned char *bg, unsigned char *fg)
{
  unsigned char whiteness, blackness;
  // A note about GIFs: the size of the color table is stored in
  // the least significant 3 bits of the 11th byte. The color table itself
  // begins on the 14th byte; it has, successively, a red byte,
  // blue byte, and green byte, and repeats. This is the color palette.
  // We replace the white-ness of the standard image with the foreground
  // color, and the blackness with the background color.
  // See also: http://www.martinreddy.net/gfx/2d/GIF89a.txt
  unsigned char *color_table_ptr = &image[13];
  unsigned char *end_color_table_ptr = color_table_ptr+3*(1 << ((image[10] & 0x7) + 1));
  // yikes, pointer arithmetic! Not really necessary, but kinda fun
  while(color_table_ptr < end_color_table_ptr) {
    whiteness = *color_table_ptr; // actually the red byte; our B&W template has equal parts r, g, and b
    blackness = ~whiteness;
    // now assign new values to the red, green, and blue bytes
    *color_table_ptr++ = (whiteness*fg[RED]  +blackness*bg[RED])  /0xff;
    *color_table_ptr++ = (whiteness*fg[GREEN]+blackness*bg[GREEN])/0xff;
    *color_table_ptr++ = (whiteness*fg[BLUE] +blackness*bg[BLUE]) /0xff;
  }
}

